package com.owc.service.impl.app;

import com.alibaba.fastjson.JSON;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.owc.common.enums.ExceptionEnum;
import com.owc.common.enums.StatusEnum;
import com.owc.common.exception.BadRequestException;
import com.owc.common.tool.*;
import com.owc.dao.*;
import com.owc.domain.BaseResult;
import com.owc.domain.WalletAddress;
import com.owc.domain.dto.*;
import com.owc.domain.entity.*;
import com.owc.domain.vo.app.TokenDetailVo;
import com.owc.domain.vo.app.TransferVo;
import com.owc.filter.content.SecurityContextHolder;
import com.owc.service.OwcService;
import com.owc.service.admin.AccountFlowService;
import com.owc.service.app.UserService;
import com.owc.service.app.WalletService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @Author Jackies
 * @Date 2019/12/3 21:42
 * @Description TODO://
 **/
@Service
@Slf4j
public class WalletServiceImpl implements WalletService {

    @Resource
    private TokenMapper tokenMapper;
    @Resource
    private TokenBalanceMapper tokenBalanceMapper;
    @Resource
    private UserMapper userMapper;
    @Resource
    private IncomeRecordMapper incomeRecordMapper;
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private UserWalletAddressMapper userWalletAddressMapper;

    @Autowired
    private OwcService owcService;
    @Autowired
    private TransactionService transactionService;
    @Resource
    private WithdrawFlowMapper withdrawFlowMapper;
    @Autowired
    private UserService userService;
    @Autowired
    private SystemConfigMapper systemConfigMapper;
    @Autowired
    private AccountFlowService accountFlowService;


    @Override
    public List<TokenInfo> getTokenList() {
        Long userId = SecurityContextHolder.getCurrentUserId();
        //Get all token list.
        List<Token> allToken = owcService.getAllToken();
        Assert.notNull(allToken,"Please initialize the token type");
        Map<Long,Token> tokenMap = BeanUtils.objectTransKeyMap(allToken,"id",Long.class);

        //Get current user token balance info.
        List<TokenInfo> tokenInfos = tokenBalanceMapper.selectUserTokenList(userId);

        if(tokenMap.size() <= tokenInfos.size()){
            return tokenInfos;
        }

        //Complete data
        tokenInfos.forEach(tokenInfo -> {
            tokenMap.remove(tokenInfo.getId());
        });

        tokenMap.values().forEach(token -> {
            String[] fields = {"id","tokenName","logo","tokenBalance","sort"};
            String[] values = {token.getId() + "",token.getTokenName(), token.getLogo() ,"0.00",token.getSort() + ""};
            try {
                tokenInfos.add(BeanUtils.buildDefaultAttrModel(fields,values,TokenInfo.class));
            } catch (Exception e) {
                log.error("BeanUtils buildDefaultAttrModel throw error. message:[{}]",e.getMessage());
            }
        });

        //Sort asc
        return tokenInfos.parallelStream().sorted(Comparator.comparing(TokenInfo::getSort)).collect(Collectors.toList());
    }

    @Override
    public BaseResult<TokenDetail> getTokenDetailList(TokenDetailVo tokenDetailVo) {
        TokenDetail tokenDetail = new TokenDetail();

        //Fetch token basis info.
        fetchTokenBasisInfo(tokenDetail,tokenDetailVo);

        //Fetch withdraw and recharge order
        PageHelper.startPage(tokenDetailVo.getPageIndex(),tokenDetailVo.getPageSize());

        PageInfo<DetailBean> pageInfo = accountFlowService.getUserFlowList(SecurityContextHolder.getCurrentUserId(),tokenDetailVo);

        tokenDetail.setDetail(pageInfo.getList());
        return BaseResult.success(tokenDetail,pageInfo.getTotal());
    }

    private void fetchTokenBasisInfo(TokenDetail tokenDetail, TokenDetailVo tokenDetailVo) {
        //Fetch token base info.
        tokenDetail.setTokenBalance(getTokenBalance(tokenDetailVo.getId(),SecurityContextHolder.getCurrentUserId()).toPlainString());
        tokenDetail.setTokenName(owcService.getTokenName(tokenDetailVo.getId()));
        tokenDetail.setTransferOut(getTokenFreezeBalance(SecurityContextHolder.getCurrentUserId(),tokenDetailVo.getId()).toPlainString());
    }

    @Override
    public Boolean transfer(TransferVo transferVo) throws Exception {
        //Check if there are orders in progress
        checkOrderProcessing();
        checkAddressFormat(transferVo.getToAddress(),transferVo.getTokenId());
        //Fetch user detail info.
        User user = userMapper.selectByPrimaryKey(SecurityContextHolder.getCurrentUserId());
        //Check if payment password is set.
        Assert.notNull(user.getPayPassword(),"Please set a payment password");
        Assert.isTrue(user.getPayPassword().equals(CryptographUtils.MD5(transferVo.getPayPassword())),"Payment password error");

        log.info("Received withdrawal request >>> userPhone:[{}],amount:[{}]",user.getPhone(),transferVo.getAmount());

        Token token = tokenMapper.selectByPrimaryKey(transferVo.getTokenId());
        Assert.notNull(token,"Exception token type：[" + transferVo.getTokenId() + "]");

        //Get current token withdraw fee amount.
        BigDecimal fee = owcService.getSystemConfig(token.getTokenName(),BigDecimal.class);
        Assert.notNull(fee,"Global configuration exception, please check the database");

        Assert.isTrue(fee.compareTo(transferVo.getAmount()) <= 0,
                "The amount cannot be less than the handling fee");

        //Fetch account token balance info.
        Assert.isTrue(getTokenBalance(transferVo.getTokenId()).getBalance()
                .compareTo(transferVo.getAmount()) >= 0,
                "Insufficient available balance");

        //Calculating total payments
        transferVo.setHandlingFee(fee);
        transferVo.setTotalPay(transferVo.getAmount());

        log.info("Withdraw valid finish. Basis info >>> fee:[{}], amount:[{}] totalPay:[{}]. Ready to save",fee,transferVo.getAmount(),transferVo.getTotalPay());

        saveWithdrawEntity(transferVo,user,token);
        return true;
    }

    private void checkOrderProcessing() {
        //Fetch current user is there in progress withdraw order.
        WithdrawFlow withdrawFlow = withdrawFlowMapper.selectProgressOrderByUserId(SecurityContextHolder.getCurrentUserId());
        if(withdrawFlow != null){
            throw new BadRequestException(ExceptionEnum.PROCESS_ORDER_ERR);
        }
    }

    private void checkAddressFormat(String toAddress, Long tokenId) {

        Token token = tokenMapper.selectByPrimaryKey(tokenId);
        if(token == null) {
            throw new BadRequestException(ExceptionEnum.INVALID_TOKEN_ERR);
        }

        //如果当前tokenId属于ETH或者BTC的下的代币,则需要根据parentId去获取父级钱包地址
        String tokenName;
        if(token.getParentId() == -1L){
            tokenName = token.getTokenName();
        }else{
            token = tokenMapper.selectByPrimaryKey(token.getParentId());
            tokenName = token.getTokenName();
        }
        boolean valid;

        if(UtilsService.isEth(tokenName)){
            valid = ValidationUtils.isEthWalletAddress(toAddress);
        }else{
            valid = ValidationUtils.isBtcWalletAddress(toAddress);
        }
        if(!valid) {
            throw new BadRequestException(ExceptionEnum.ADDRESS_FORMAT_ERR);
        }
    }

    @Override
    public BalanceDTO getTokenBalance(Long tokenId) {
        return getTokenBalanceByUserId(tokenId,SecurityContextHolder.getCurrentUserId());
    }

    @Override
    public BalanceDTO getTokenBalanceByUserId(Long tokenId, Long userId) {
        return new BalanceDTO(getTokenBalance(tokenId,userId),getTokenNameById(tokenId));
    }

    private String getTokenNameById(Long tokenId){
        Token token = tokenMapper.selectByPrimaryKey(tokenId);
        if(token == null){
            return null;
        }
        return token.getTokenName();
    }

    @Override
    public BigDecimal getWithdrawAmountByToken(Long tokenId, Long userId) {
        return Optional.ofNullable(withdrawFlowMapper.selectPendingWithdrawAmount(tokenId,userId))
                .orElse(BigDecimal.ZERO);
    }

    @Override
    public TokenAddress getTokenAddress(@NonNull Long tokenId) {
        Token token = tokenMapper.selectByPrimaryKey(tokenId);
        Assert.notNull(token,"Exception tokenId");

        String tokenName = token.getTokenName();

        if(!"-1".equals(token.getParentId().toString())){
            //Query parent token name.
            token = tokenMapper.selectByPrimaryKey(token.getParentId());
            Assert.notNull(token,"Exception tokenId");
        }

        Optional<User> optionalUser = userService.selectByUserName(SecurityContextHolder.getCurrentUserName());
        if(!optionalUser.isPresent()){
            throw new BadRequestException(ExceptionEnum.USER_INVALID_ERR);
        }
        WalletAddress walletAddress = JSON.parseObject(optionalUser.get().getWalletAddress(), WalletAddress.class);
        if(UtilsService.isEth(token.getTokenName())){
            return new TokenAddress(walletAddress.getEth(),tokenName);
        }else{
            return new TokenAddress(walletAddress.getBtc(),tokenName);
        }
    }

    @Override
    public TokenHandlingFee getTokenHandlingFee(Long tokenId) {
        Token token = tokenMapper.selectByPrimaryKey(tokenId);
        Assert.notNull(token,"Exception tokenId");
        String rate = systemConfigMapper.selectByKey(token.getTokenName());
        return new TokenHandlingFee(new BigDecimal(rate));
    }

    @Override
    public FlowDetail getFlowDetail(@NonNull String flowId) {
        return withdrawFlowMapper.selectFlowDetail(flowId);
    }

    private BigDecimal getTokenBalance(Long tokenId, Long userId){
        return Optional.ofNullable(tokenBalanceMapper.selectByUserIdAndTokenId(userId,tokenId))
                .map(TokenBalance::getBalance)
                .orElse(BigDecimal.ZERO);
    }

    @Override
    public UserIncome getUserIncomeInfo() {
        //获取用户昨日收益
        BigDecimal yesterdayIncome = incomeRecordMapper.selectYesterdayIncome(SecurityContextHolder.getCurrentUserId());
        //获取用户总收益
        BigDecimal totalIncome = orderMapper.selectUserTotalIncome(SecurityContextHolder.getCurrentUserId());
        return new UserIncome(yesterdayIncome,totalIncome);
    }

    @Override
    public Token getTokenByName(String tokenName) {
        return tokenMapper.selectByTokenName(tokenName);
    }

    @Override
    public BigDecimal getTokenFreezeBalance(Long tokenId, Long userId) {
        Assert.notNull(tokenId,"TokenId must not be null.");
        TokenBalance tokenBalance = tokenBalanceMapper.selectByUserIdAndTokenId(tokenId,userId);
        return tokenBalance == null ? BigDecimal.ZERO : tokenBalance.getFreeze();
    }

    @Override
    public Long getUserByAddress(String address) {
        Assert.notNull(address,"GetUserIdByAddress, Address must not be null.");
        return userWalletAddressMapper.selectByEthAddress(address);
    }

    private void saveWithdrawEntity(TransferVo transferVo, User user, Token token) throws Exception {

        WithdrawFlow withdrawFlow;

        UserWalletAddress userWalletAddress = userWalletAddressMapper.selectByUserId(user.getId());

        String[] fields = {"transferId","walletAddress","realAmount","handlingFee",
                            "totalAmount","tokenId","tokenName","commitTime","status","phone","handlingFee","userId","fromAddress"};

        Object[] values = {UtilsService.getGuid(),transferVo.getToAddress(),transferVo.getAmount().subtract(transferVo.getHandlingFee()),transferVo.getHandlingFee(),
                            transferVo.getTotalPay(),transferVo.getTokenId(),token.getTokenName(), DateUtils.formatYMDHMS(new Date()),
                            StatusEnum.PENDING.getType(),user.getPhone(),transferVo.getHandlingFee(),user.getId(),userWalletAddress.getEth()};

        try {
            withdrawFlow = BeanUtils.buildDefaultAttrModel(fields,values,WithdrawFlow.class);
        } catch (Exception e) {
            log.error("Save withdraw entity failed. Trans model throw error. message:[{}]",e.getMessage());
            throw new BadRequestException(ExceptionEnum.UNKNOWN_ERR);
        }
        transactionService.createWithdrawEntity(withdrawFlow);
    }
}
